<template>
  <q-page class="flex flex-center">
    <SideBar v-model:model-value="leftDrawerOpen" width="260">
      <template #body>
        <div class="absolute-top button-container">
          <q-btn
            flat
            rounded
            @click="
              () => {
                leftDrawerOpen = !leftDrawerOpen;
              }
            "
          >
            <q-icon name="img:/assets/drawer_ctrl.svg" />
          </q-btn>
          <q-btn flat rounded @click="NewSession">
            <q-icon name="img:/assets/editor.svg" />
          </q-btn>
        </div>

        <div class="absolute-bottom bottom-container row">
          <q-btn
            v-if="!isSearch"
            class="col"
            flat
            href="https://chatgpt.com/"
            target="_blank"
            no-caps
          >
            <q-icon
              name="img:/assets/shrink.svg"
              size="20px"
              style="margin-right: 5px"
            />
            <span style="color: #5d5d5d">订阅Plus</span>
          </q-btn>

          <q-input
            @focus="isSearch = true"
            @blur="isSearch = false"
            dense
            standout="bg-grey text-white"
            v-model="searchText"
            @update:model-value="getSessions"
            input-class="text-right"
            class="q-ml-md col"
            style="margin-right: 5px"
          >
            <template v-slot:append>
              <q-icon v-if="searchText === ''" name="search" />
              <q-icon
                v-else
                name="clear"
                class="cursor-pointer"
                @click="
                  () => {
                    searchText = '';
                    getSessions();
                  }
                "
              />
            </template>
          </q-input>
        </div>

        <q-scroll-area class="drawer-box">
          <q-list padding>
            <q-item clickable v-ripple to="chatgpt">
              <q-item-section avatar>
                <q-icon name="img:/assets/chatgpt.svg" size="30px" />
              </q-item-section>

              <q-item-section>ChatGPT</q-item-section>
            </q-item>

            <q-item clickable v-ripple to="deepseek">
              <q-item-section avatar>
                <q-icon name="img:/assets/deepseek.png" size="30px" />
              </q-item-section>

              <q-item-section>DeepSeek</q-item-section>
            </q-item>

            <q-item clickable v-ripple to="video-search">
              <q-item-section avatar>
                <q-icon name="img:/assets/video.svg" size="30px" />
              </q-item-section>

              <q-item-section>影视搜索</q-item-section>
            </q-item>

            <q-item clickable v-ripple to="application">
              <q-item-section avatar>
                <q-icon
                  color="blue"
                  name="fa-brands fa-app-store-ios"
                  size="30px"
                />
              </q-item-section>

              <q-item-section>更多程序</q-item-section>
            </q-item>
            <q-separator />
            <q-item-label header>最近更新</q-item-label>
            <q-item
              clickable
              v-ripple
              v-for="item in history.recentDisplay"
              :key="item.id"
              :active="sessionId === item.id"
              active-class="bg-grey-1 text-grey-10"
              class="text-grey-8 truncate-single-line"
              @click="ChangeSession(item.id)"
            >
              <q-item-section>{{ item.name }}</q-item-section>
              <q-tooltip anchor="center right" self="center left">
                {{ item.updateTime }}
              </q-tooltip>
            </q-item>
            <q-item
              clickable
              v-ripple
              dense
              v-if="history.recentDisplay.length < history.recent.length"
              @click="
                addDataForHistory(5, history.recent, history.recentDisplay)
              "
            >
              加载更多...
            </q-item>
            <history-section
              v-if="history.threeDaysAgo.length > 0"
              label="3天内"
              :items="history.threeDaysAgo"
              :itemsDisplay="history.threeDaysAgoDisplay"
              :sessionId="sessionId"
              :addDataForHistory="addDataForHistory"
              :changeSession="ChangeSession"
            />

            <history-section
              v-if="history.sevenDaysAgo.length > 0"
              label="7天内"
              :items="history.sevenDaysAgo"
              :itemsDisplay="history.sevenDaysAgoDisplay"
              :sessionId="sessionId"
              :addDataForHistory="addDataForHistory"
              :changeSession="ChangeSession"
            />

            <history-section
              v-if="history.older.length > 0"
              label="更久"
              :items="history.older"
              :itemsDisplay="history.olderDisplay"
              :sessionId="sessionId"
              :addDataForHistory="addDataForHistory"
              :changeSession="ChangeSession"
            />
          </q-list>
        </q-scroll-area>
      </template>
    </SideBar>

    <TopBar>
      <template #left>
        <div
          class="absolute-top"
          style="margin-top: 10px"
          v-if="!leftDrawerOpen"
        >
          <q-btn
            flat
            rounded
            @click="
              () => {
                leftDrawerOpen = !leftDrawerOpen;
              }
            "
          >
            <q-icon name="img:/assets/drawer_ctrl.svg" />
          </q-btn>
          <q-btn flat rounded @click="NewSession">
            <q-icon name="img:/assets/editor.svg" />
          </q-btn>
        </div>

        <q-btn
          class="page-title"
          dense
          flat
          no-caps
          :style="{ marginLeft: `${leftDrawerOpen ? '0px' : '100px'}` }"
        >
          <span
            >ChatGPT
            <span v-if="sessionModel"> ({{ sessionModel }}) </span></span
          >
        </q-btn>
      </template>

      <template #right>
        <q-btn flat dense v-if="userInfo !== null">
          <q-avatar color="yellow-8" text-color="white" size="30px">
            <span v-if="!userInfo.avatar_url">{{ userInfo.username[0] }}</span>
            <q-img v-else :src="userInfo.avatar_url" />
          </q-avatar>
        </q-btn>
      </template>
    </TopBar>

    <div id="chat-gpt-body" style="width: 100%" class="flex flex-center">
      <h1 v-if="messages.current.length === 0">有什么可以帮忙的？</h1>
      <div
        v-else
        :style="{
          width: '100%',
          height: `${$q.screen.height - 150 - (chatInputHeight - 56)}px`,
        }"
        class="message-box-parent"
      >
        <q-virtual-scroll
          style="height: 100%"
          :items="messages.current"
          id="messages-box"
          separator
          v-slot="{ item }"
        >
          <q-chat-message
            readonly
            :key="item.guid"
            :text="[item.text]"
            :sent="item.sent"
            :text-color="item.textColor"
            :bg-color="item.bgColor"
          >
            <span v-if="item.sent">
              <q-img
                v-if="item.img !== ''"
                :src="item.img"
                style="min-width: 250px"
              >
                <div class="absolute-bottom-right text-subtitle2">
                  {{ item.text }}
                </div>
              </q-img>
              <span v-else>{{ item.text }}</span>
            </span>
            <span v-else :id="item.guid" v-bind:key="item.guid">
              <q-img width="250px" :src="item.img" v-if="item.img !== ''" />

              <q-spinner-ios
                v-if="item.text === ''"
                color="grey-5"
                size="2em"
              />
              <MdPreview v-model="item.text" v-else />
              <q-btn
                icon="refresh"
                rounded
                flat
                @click="RetryResponse"
                v-if="
                  item.text === '异常，请重试' &&
                  !item.sent &&
                  chatCache.aiMessage.guid === item.guid
                "
              />
            </span>
          </q-chat-message>
        </q-virtual-scroll>
      </div>
      <div
        :class="
          messages.current.length === 0
            ? 'chat-input-one flex flex-center'
            : 'absolute-bottom chat-input flex flex-center'
        "
      >
        <q-input
          ref="chatInputRef"
          :loading="isLoading"
          :disable="isDisable"
          v-model:model-value="inputMessage"
          @keyup.enter="SendMessage"
          label="给ChatGPT发送消息"
          @paste="handlePaste"
          rounded
          outlined
          input-class="chat-input-style"
          class="chat-input-style-out"
          autofocus
        >
          <template v-slot:prepend>
            <q-btn flat v-if="ImageUrl">
              <q-popup-proxy style="width: 250px">
                <q-card style="width: 100%; max-width: 250px">
                  <q-img :src="ImageUrl"> </q-img>
                  <q-card-actions>
                    <q-btn
                      flat
                      @click="
                        () => {
                          ImageUrl = '';
                        }
                      "
                    >
                      移除
                    </q-btn>
                  </q-card-actions>
                </q-card>
              </q-popup-proxy>
              <q-avatar size="30px">
                <q-img :src="ImageUrl" />
              </q-avatar>
            </q-btn>
          </template>

          <template v-slot:append>
            <q-btn
              flat
              dense
              rounded
              @click="SendMessage"
              :loading="isLoading"
              :disable="isDisable"
            >
              <q-icon name="fa-solid fa-circle-up" />
            </q-btn>
          </template>
        </q-input>
      </div>
    </div>

    <BeiAn text="ChatGPT 也可能会犯错。请核查重要信息。" />
  </q-page>
</template>

<script setup>
import BeiAn from "src/components/BeiAn.vue";
import SideBar from "src/components/SideBar.vue";
import TopBar from "components/TopBar.vue";
import { ref, onMounted, reactive, watch, onBeforeUnmount } from "vue";
import { session, api } from "src/boot/axios.js";
import { useQuasar } from "quasar";
import notify from "src/utility/notify";
import { common, createStarryNight } from "@wooorm/starry-night";
import URLSearch from "src/utility/URLSearch";
import { uuidv4 } from "src/utility/StringUtil.js";
import MarkdownIt from "markdown-it";
import "github-markdown-css";
import { toHtml } from "hast-util-to-html";
import HistorySection from "src/components/HistorySection.vue";
import { MdPreview } from "md-editor-v3";
import "md-editor-v3/lib/preview.css";

const chatInputRef = ref(null);
const resizeObserver = ref(null);
const chatInputHeight = ref(56);

const isSearch = ref(false);
const searchText = ref("");
const $q = useQuasar();
const leftDrawerOpen = ref(true);
const userInfo = ref(null);
const starryNight = ref(null);

const inputMessage = ref("");
const messages = reactive({
  current: [],
});
const ImageUrl = ref("");
const isLoading = ref(false);
const isDisable = ref(true);

const sessionId = ref(null);
const history = reactive({
  recent: [],
  recentDisplay: [],
  threeDaysAgo: [],
  threeDaysAgoDisplay: [],
  sevenDaysAgo: [],
  sevenDaysAgoDisplay: [],
  older: [],
  olderDisplay: [],
});

const ChangeSession = async (session_id) => {
  URLSearch.clearQueryParams();
  sessionId.value = session_id;
  messages.current = [];
  await onload();
  await getSessions();
};

const addDataForHistory = (count, sourceList, targetList) => {
  const startIndex = targetList.length;
  let endIndex = targetList.length + count;
  if (endIndex > sourceList.length) {
    endIndex = sourceList.length;
  }

  let subArray = sourceList.slice(startIndex, endIndex);
  targetList.push(...subArray);

  console.log(sourceList, targetList);
};

const NewSession = () => {
  URLSearch.clearQueryParams();
  sessionId.value = null;
  messages.current = [];
  onload();
  getSessions();
};

const handlePaste = (event) => {
  const clipboardData = event.clipboardData || window.clipboardData;
  const items = clipboardData.items;

  for (let i = 0; i < items.length; i++) {
    if (items[i].type.indexOf("image") !== -1) {
      const file = items[i].getAsFile();

      const reader = new FileReader();
      reader.onload = (event) => {
        const base64URL = event.target.result;
        ImageUrl.value = base64URL;
        console.log(ImageUrl.value);
      };
      reader.readAsDataURL(file);
    }
  }
};

const SendMessage = async () => {
  const message = inputMessage.value;
  if (/^\s*$/.test(message)) {
    inputMessage.value = "";
    return;
  }

  if (sessionId.value === null || sessionId.value === undefined) {
    notify.warn("还没有建立会话，请刷新页面");
    return;
  }

  pushMessage(message, true, new Date(), uuidv4(), ImageUrl.value);
  inputMessage.value = "";
  isLoading.value = true;
  const aiMessage = pushMessage("", false, new Date(), uuidv4());

  try {
    const chat_id = (
      await api.post("/ai/chat", {
        session_id: sessionId.value,
        q: message,
        i: ImageUrl.value,
      })
    ).data.data.chat_id;
    await FetchResponseByChatId(chat_id, aiMessage);
    // create new session
  } catch (ex) {
    if (ex.response !== undefined && ex.response.data.message) {
      notify.warn(ex.response.data.message);
    } else {
      notify.error(`请求失败: ` + ex.message);
      console.error(ex);
    }
    setMessage(aiMessage, "异常，请重试");
  } finally {
    isLoading.value = false;
    ImageUrl.value = "";
  }
};

const setMessage = (aiMessage, text) => {
  aiMessage.text = text;
  try {
    aiMessage.display = markdownItInstance.render(aiMessage.text);
  } catch {
    aiMessage.display = aiMessage.text;
  }
};

const chatCache = reactive({
  chatId: null,
  aiMessage: null,
});
const chunks = ref("");

const RetryResponse = () => {
  if (chatCache.aiMessage === null || chatCache.chatId === null) return;

  chatCache.aiMessage.text = "";
  isLoading.value = true;
  FetchResponseByChatId(chatCache.chatId, chatCache.aiMessage);
};

const FetchResponseByChatId = async (chatId, aiMessage) => {
  chunks.value = "";
  chatCache.chatId = chatId;
  chatCache.aiMessage = aiMessage;
  const baseUrl =
    process.env.NODE_ENV === "production"
      ? "/api/v1"
      : "http://127.0.0.1:5000/api/v1";

  try {
    const response = await fetch(
      baseUrl + `/ai/chat-process?chat_id=${chatId}`,
      {
        method: "GET",
      },
    );
    if (!response.ok) {
      console.error("Failed to fetch response:", response.statusText);
      notify.error(`建立Stream会话失败: ` + response.statusText);
      setMessage(aiMessage, "异常，请重试");
      return;
    }
    const reader = response.body.getReader();
    const decoder = new TextDecoder("utf-8");

    // Read the stream chunk by chunk
    while (true) {
      const { value, done } = await reader.read();
      if (done) break; // Stream has ended

      const chunk = decoder.decode(value, { stream: true });

      try {
        const errorRes = JSON.parse(chunk);
        if (errorRes.error) {
          notify.error(`会话响应解析异常: ` + errorRes.message);
          console.error(ex);
          setMessage(aiMessage, "异常，请重试");
        }
      } catch {}

      if (!chunk.includes(`[${chatId}-DONE]`)) {
        chunks.value += chunk;
        setMessage(aiMessage, chunks.value);
      }
    }
  } catch (ex) {
    if (ex.response !== undefined && ex.response.data.message) {
      console.error(ex.response.data.message);
      notify.warn(ex.response.data.message);
    } else {
      notify.error(`会话通讯异常: ` + ex.message);
      console.error(ex);
    }
    setMessage(aiMessage, "异常，请重试");
  } finally {
    isLoading.value = false;
    getSessions();
    setTimeout(() => {
      ToBottom();
    }, 100);
  }
};

const pushMessage = (text, sent, timestamp, guid, img = "") => {
  let display = text;
  try {
    display = markdownItInstance.render(text);
  } catch {}
  const theMessage = {
    text: text,
    sent,
    textColor: "blank",
    bgColor: sent ? "grey-1" : "grey-2",
    timestamp,
    guid,
    display,
    img,
  };

  messages.current.push(theMessage);

  setTimeout(() => {
    ToBottom();
  }, 100);
  return theMessage;
};

const ToBottom = () => {
  const messagesBox = document.getElementById("messages-box");
  if (messagesBox) {
    messagesBox.scrollTop = messagesBox.scrollHeight;
  }
};

const markdownItInstance = MarkdownIt({
  highlight(value, lang) {
    const scope = starryNight.value.flagToScope(lang);

    // 使用toHtml包裹生成HTML结构，并添加自定义标识符
    return toHtml({
      type: "element",
      tagName: "span", // 包裹在一个div中，方便插入复制按钮
      properties: {
        className: "code-block-container",
      },
      children: [
        {
          type: "element",
          tagName: "button",
          properties: {
            type: "button",
            className: "copy-button",
            // 在自定义属性中存储代码值
            "data-code": value,
            onclick: `copyToClipboard(this)`,
          },
          children: [{ type: "text", value: "Copy" }],
        },
        {
          type: "element",
          tagName: "pre",
          properties: {
            className: scope
              ? [
                  "highlight",
                  "highlight-" +
                    scope.replace(/^source\./, "").replace(/\./g, "-"),
                ]
              : undefined,
            title: lang,
          },
          children: scope
            ? /** @type {Array<ElementContent>} */ (
                starryNight.value.highlight(value, scope).children
              )
            : [{ type: "text", value }],
        },
      ],
    });
  },
});

const sessionModel = ref("");

const onload = async () => {
  $q.loading.show();

  const params = URLSearch.getAllQueryParams();
  if (params && params.session_id) {
    sessionId.value = params.session_id;
  }

  // get session data by id
  if (sessionId.value !== null) {
    try {
      const sessionResponse = await api.get(
        "/ai/session?session_id=" + sessionId.value,
      );
      sessionModel.value = sessionResponse.data.model;
      const history = sessionResponse.data.items;
      history.forEach((item) => {
        pushMessage(item.user, true, item.timestamp, uuidv4(), item.img);
        pushMessage(item.ai, false, item.timestamp, uuidv4());
      });
    } catch (ex) {
      if (ex.response !== undefined && ex.response.data.message) {
        console.error(ex.response.data.message);
        notify.warn(ex.response.data.message);
      } else {
        notify.error(`获取会话资源失败: ` + ex.message);
        console.error(ex);
      }
      sessionId.value = null;
      URLSearch.clearQueryParams();
    }
  }

  if (sessionId.value === null) {
    try {
      const session_id = (await api.post("/ai/session")).data.data.session_id;
      sessionId.value = session_id;
    } catch (ex) {
      if (ex.response !== undefined && ex.response.data.message) {
        console.error(ex.response.data.message);
        notify.warn(ex.response.data.message);
      } else {
        notify.error(`创建会话资源失败: ` + ex.message);
        console.error(ex);
      }
    }
  }

  if (sessionId.value !== null) {
    URLSearch.addOrUpdateQueryParam("session_id", sessionId.value);
  }

  $q.loading.hide();
};

const getSessions = async () => {
  history.recent = [];
  history.recentDisplay = [];
  history.threeDaysAgo = [];
  history.threeDaysAgoDisplay = [];
  history.sevenDaysAgo = [];
  history.sevenDaysAgoDisplay = [];
  history.older = [];
  history.olderDisplay = [];
  try {
    const items = (await api.get("/ai/?search=" + searchText.value)).data.items;
    const now = new Date();

    items.forEach((item) => {
      const updateTime = new Date(item.updateTime);
      const timeDiff = now - updateTime; // 毫秒差

      const oneDay = 24 * 60 * 60 * 1000; // 一天的毫秒数
      const threeDays = 3 * oneDay;
      const sevenDays = 7 * oneDay;

      if (timeDiff <= oneDay) {
        history.recent.push(item); // 过去1天内
      } else if (timeDiff <= threeDays) {
        history.threeDaysAgo.push(item); // 已超过1天，但在3天内
      } else if (timeDiff <= sevenDays) {
        history.sevenDaysAgo.push(item); // 已超过3天，但在7天内
      } else {
        history.older.push(item);
      }
    });
  } catch (ex) {
    if (ex.response !== undefined && ex.response.data.message) {
      console.error(ex.response.data.message);
      notify.warn(ex.response.data.message);
    } else {
      notify.error(`获取话题历史记录失败: ` + ex.message);
      console.error(ex);
    }
  } finally {
    addDataForHistory(5, history.recent, history.recentDisplay);
    addDataForHistory(3, history.threeDaysAgo, history.threeDaysAgoDisplay);
    addDataForHistory(3, history.sevenDaysAgo, history.sevenDaysAgoDisplay);
    addDataForHistory(3, history.older, history.olderDisplay);
  }
};

watch(sessionId, () => {
  isDisable.value = sessionId.value === null;
});

onMounted(async () => {
  api.defaults.headers.common["Ai-Model"] = "chatgpt-4o-latest";
  if (chatInputRef.value) {
    // 创建一个ResizeObserver观察器实例
    resizeObserver.value = new ResizeObserver((entries) => {
      for (let entry of entries) {
        // 更新高度值
        chatInputHeight.value = entry.contentRect.height;
      }
    });
    // 开始观察quasar输入组件
    resizeObserver.value.observe(chatInputRef.value.$el); // 注意观察的是组件的$el
  }

  $q.loading.show();
  starryNight.value = await createStarryNight(common);
  userInfo.value = await session.get("userInfo");
  if (userInfo.value !== null) {
    await onload();
    await getSessions();
  }
  $q.loading.hide();
});

onBeforeUnmount(() => {
  if (resizeObserver.value !== null) {
    resizeObserver.value.disconnect();
  }
});
</script>

<style>
.button-container {
  display: flex;
  justify-content: space-between; /* 在容器的两端排列子元素 */
  align-items: center; /* 垂直居中对齐 */
  height: 56px; /* 设定与父容器相同的高度 */
  margin-left: 8px;
  margin-right: 8px;
}

。bottom-container {
  margin-left: 8px;
  margin-right: 8px;
  height: 40px;
}

.drawer-box {
  height: calc(100% - 106px);
  margin-top: 56px;
  padding-left: 7px;
  padding-right: 7px;
  font-size: 0.875rem;
  line-height: 1.25rem;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: #0d0d0d;
}

.q-item__section--avatar {
  min-width: 0px !important;
}

.page-title {
  color: #5d5d5d;
  font-weight: 600;
  font-size: 1.125rem;
  margin-top: 5px;
  line-height: 1.75rem;
}

h1 {
  font-size: 1.875rem;
  font-weight: 600;
  line-height: 2.25rem;
}

.chat-input {
  padding-bottom: 20px;
}

.message-box-parent {
  top: 0px;
  position: absolute;
  padding-left: 20px;
  padding-right: 20px;
  max-width: 1200px;
}

.chat-input-one {
  width: 100%;
}

.q-message-container > div {
  max-width: 100%;
}

pre {
  white-space: pre-wrap; /* 允许自动换行 */
}

.markdown-body {
  box-sizing: border-box;
  font-size: normal;
  border-radius: 15px;
  background-color: rgb(244 244 244 / var(--tw-bg-opacity));
  margin: 0 auto;
}

@media (max-width: 767px) {
  .markdown-body {
    padding: 15px;
  }
}

.q-message-text {
  /* background: #e8e8e8; */
  border-radius: 15px;
  font-size: 1rem;
  line-height: 1.5rem;
  padding: 20px;
  padding-top: 10px;
  padding-bottom: 10px;
}

#messages-box::-webkit-scrollbar {
  width: 8px; /* 滚动条宽度 */
}

#messages-box::-webkit-scrollbar-track {
  background: #cccccc00; /* 滚动条轨道颜色 */
  border-radius: 4px; /* 圆角 */
}

#messages-box::-webkit-scrollbar-thumb {
  background-color: #cccccc00; /* 滚动条颜色 */
  border-radius: 4px; /* 圆角 */
  border: 2px solid #cccccc00; /* 滚动条和轨道之间的间隙颜色 */
}

/* 针对 Firefox */
#messages-box {
  scrollbar-width: thin; /* 滚动条宽度 */
  scrollbar-color: #cccccc00 #e8e8e800;
}

* {
  scrollbar-color: var(#cccccc00) transparent;
}

.chat-input-style {
  width: 100%;
  scrollbar-width: thin; /* 滚动条宽度 */
  scrollbar-color: #cccccc00 #e8e8e800;
}

.chat-input-style-out {
  width: 80%;
  max-width: 1200px;
  scrollbar-width: thin; /* 滚动条宽度 */
  scrollbar-color: #cccccc00 #e8e8e800;
}

.truncate-single-line {
  white-space: nowrap; /* 禁止换行 */
  overflow: hidden; /* 隐藏溢出内容 */
  text-overflow: ellipsis; /* 使用省略号表示溢出的文本 */
}

.q-scrollarea__content {
  max-width: 260px;
}

.code-block-container {
  position: relative;
}

.copy-button:hover {
  background: #e2e2e2;
}
</style>
